﻿namespace Bowlus.FrontEnd.RequestRouting
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reactive.Linq;

    using Bowlus.Common;
    using Bowlus.Common.DeploymentActions;
    using Bowlus.FrontEnd.RequestRouting.ApplicationDeployment;
    using Bowlus.FrontEnd.RequestRouting.BlobLeaseUtil;
    using Microsoft.WindowsAzure.ServiceRuntime;

    using ApplicationHost = Bowlus.Common.ApplicationHost;

    public class RoutingManager
    {
        public const string DefaultLeaseBlobName = "routingmanager";

        private readonly IRoutingConfigManager arrConfigManager;

        private readonly IApplicationDeploymentStrategy applicationDeploymentStrategy;

        private readonly IObservable<long> leaseRenewTimer;

        private readonly IBlobLease blobLease;

        private readonly ConcurrentDictionary<string, ApplicationHost> applicationHosts;

        public RoutingManager(
            IRoutingConfigManager arrConfigManager,
            IApplicationDeploymentStrategy deploymentStrategy,
            IBlobLease blobLease)
        {
            this.arrConfigManager = arrConfigManager;
            this.applicationDeploymentStrategy = deploymentStrategy;
            this.blobLease = blobLease;
            this.applicationHosts = new ConcurrentDictionary<string, ApplicationHost>();

            this.leaseRenewTimer = Observable.Interval(TimeSpan.FromSeconds(50));
        }

        public event EventHandler<RequestRoutingManagerEventArgs> StatusChange;

        private bool IsMaster
        {
            get
            {
                return this.blobLease.LeaseStatus == BlobLeaseStatus.Acquired || RoleEnvironment.IsEmulated;
            }
        }

        public void Initialize()
        {
            // Block OnStart until all apphosts are ready.
            BowlusHostingEnvironment.WaitTillAllAppHostsAreRunning();

            var appHosts = BowlusHostingEnvironment.GetCurrentAppHosts().ToList();

            var runningAppHosts = appHosts.Where(ah => ah.Status.Equals(ApplicationHostStatus.Running)).ToList();

            // Initialize the local list of app hosts
            foreach (var applicationHost in runningAppHosts.Where(applicationHost => !this.applicationHosts.TryAdd(
                applicationHost.ApphostHttpEndpoint.Address.ToString(), applicationHost)))
            {
                BowlusLogger.LogError(string.Format("Cannot add appHost {0} to the dictionary.", applicationHost.ApphostHttpEndpoint.Address));
            }

            // Common initialize work for first and subsequent instances. Setup the local routing environment.
            this.arrConfigManager.Initialize();

            this.arrConfigManager.UpdateApplicationHostsConfiguration(runningAppHosts);
            var tenantDeployments = TenantDeploymentsStore.Instance.TenantDeployment.ToList();

            this.blobLease.AcquireLease();

            if (!this.IsMaster)
            {
                BowlusLogger.LogInfo("Not the first instance of the app host.");

                // No need to initiate a redeployment, since this is the subsequent instance. Just update the routing rules
                // TODO: This is not correct, what happens if the applications are not yet deployed, i.e., not in the 'running' state?
                var runningTenantDeployments = (from deployment in tenantDeployments
                                                where deployment.Status.Equals(TenantDeploymentStatus.Running)
                                                select deployment).ToList();

                this.arrConfigManager.UpdateRoutingRules(runningTenantDeployments);

                this.OnStatusChange(Guid.Empty, "Not the first instance, just updated the routing configuration.");
                return;
            }

            BowlusLogger.LogInfo("First instance, managed to acquire the lease.");

            // Subscribe to the timer and start it
            this.leaseRenewTimer.Subscribe(s => this.blobLease.RenewLease());

            // Change the status of all the instances to initializing
            foreach (var tenantDeployment in tenantDeployments)
            {
                tenantDeployment.Status = TenantDeploymentStatus.Initializing;
            }

            TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployments);

            var tenantChanges = this.applicationDeploymentStrategy.InitializeDeployments(runningAppHosts, tenantDeployments);

            this.UpdateTenantDeployments(tenantChanges);
        }

        public void ClearConfiguration()
        {
            this.arrConfigManager.ClearConfiguration();

        }

        public void OnAppHostsChange()
        {
            try
            {
                // Do not just get running but get all...
                var currentAppHosts = BowlusHostingEnvironment.GetCurrentAppHosts().ToList();
                var knownAppHosts = this.applicationHosts.Keys;

                // First mark those that need to be deleted
                var deletingHosts = from appHost in currentAppHosts
                                    where appHost.Status.Equals(ApplicationHostStatus.Deleting)
                                    select appHost.ApphostHttpEndpoint.Address.ToString();

                foreach (var host in deletingHosts.Where(host => this.applicationHosts.Keys.Contains(host)))
                {
                    this.applicationHosts[host].Status = ApplicationHostStatus.Deleting;
                }

                var currentAppHostWithASpecificStatus = from currentAppHost in currentAppHosts
                                                        where !currentAppHost.Status.Equals(ApplicationHostStatus.Deleting)
                                                        select currentAppHost.ApphostHttpEndpoint.Address.ToString();

                var hostsRemoved = knownAppHosts.Except(currentAppHostWithASpecificStatus).ToList();

                foreach (var host in hostsRemoved.Where(host => this.applicationHosts.Keys.Contains(host)))
                {
                    this.applicationHosts[host].Status = ApplicationHostStatus.Deleting;
                }

                // Find new ones, if there are any
                var runningHosts = from host in currentAppHosts
                                                    where host.Status.Equals(ApplicationHostStatus.Running)
                                                    select
                                                        new
                                                            {
                                                                HostAddress = host.ApphostHttpEndpoint.Address.ToString(),
                                                                AppHost = host
                                                            };
                var newRunningHosts = runningHosts.Where(ah => !knownAppHosts.Any(h => h.Equals(ah.HostAddress))).ToList();
                foreach (var host in newRunningHosts)
                {
                    if (this.applicationHosts.Keys.Contains(host.HostAddress))
                    {
                        this.applicationHosts[host.HostAddress].Status = ApplicationHostStatus.Added;
                    }
                    else
                    {
                        host.AppHost.Status = ApplicationHostStatus.Added;
                        this.applicationHosts.TryAdd(host.HostAddress, host.AppHost);
                    }
                }

                foreach (var applicationHost in this.applicationHosts.Values)
                {
                    BowlusLogger.LogInfo(string.Format("ApplicationHost change details: {0}:{1}", applicationHost.InstanceId, applicationHost.Status));
                }

                // For those ones with states other than deleting or running, set their timers to be updated eventually
                currentAppHostWithASpecificStatus = from host in currentAppHosts
                                                    where !(host.Status.Equals(ApplicationHostStatus.Running) || host.Status.Equals(ApplicationHostStatus.Deleting))
                                                    select host.ApphostHttpEndpoint.Address.ToString();
                var transitioningHosts = currentAppHostWithASpecificStatus.Except(knownAppHosts).ToList();
                foreach (var host in transitioningHosts.Where(host => this.applicationHosts.Keys.Contains(host)))
                {
                    this.applicationHosts[host].StartPollingStatus();
                }
            }
            catch (Exception exception)
            {
                BowlusLogger.LogError(string.Format("EXCEPTION: OnAppHostChange {0}", exception));
            }
        }

        public void Run()
        {
            try
            {
                // Need to process host removals to remove them from the routing tables.
                // Check for master is in the method before making the permanent changes to the 
                // TenantDeployment table.
                var count = this.ProcessAppHostRemovals();
                BowlusLogger.LogInfoIf(string.Format("Removed {0} appHosts", count), count > 0);

                if (this.IsMaster)
                {
                    count = this.ProcessAppHostAdditions();
                    BowlusLogger.LogInfoIf(string.Format("Added {0} appHosts", count), count > 0);

                    count = this.ProcessApplicationRemovals();
                    BowlusLogger.LogInfoIf(string.Format("Removed {0} applications", count), count > 0);

                    count = this.ProcessApplicationAdditions();
                    BowlusLogger.LogInfoIf(string.Format("Added {0} applications", count), count > 0);
                }

                // Do this last to make sure we reroute most we can at the end of an iteration
                this.ProcessReroutes();
				this.ProcessRemoves();
				this.ProcessMoves();
			}
            catch(Exception ex)
            {
                BowlusLogger.LogError(string.Format("Unexpected error while processing state: {0}", ex.Message));
                throw;
            }
        }

        private void ProcessMoves()
        {
			var tenantDeployments = TenantDeploymentsStore.Instance.TenantDeployment.ToList();

			var applicationsMoved = (from tenantDeployment in tenantDeployments
									  where tenantDeployment.DeploymentAction.Equals(DeploymentActionType.Move)
									  select tenantDeployment).ToList();

			if (!applicationsMoved.Any()) return;

			foreach (var newApplication in applicationsMoved)
			{
				var currentAppHosts = this.CurrentAppHosts(tenantDeployments);
				var requests = this.applicationDeploymentStrategy.AddApplication(newApplication, currentAppHosts);
				foreach (var request in requests)
				{
					request.MakeDeploymentRequest();
				}
			}
		}

        private void ProcessRemoves()
        {
            var tenantDeployments = TenantDeploymentsStore.Instance.TenantDeployment.ToList();

            var removedDeployments = (from tenantDeployment in tenantDeployments
                                      where tenantDeployment.DeploymentAction.Equals(DeploymentActionType.Remove)
                                      select tenantDeployment).ToList();

            if (!removedDeployments.Any()) return;

            var tenantDeploymentsAfterRemoved = (from tenantDeployment in tenantDeployments
                                                 where !tenantDeployment.DeploymentAction.Equals(DeploymentActionType.Remove)
                                                 select tenantDeployment).ToList();

            var hostsAfterRemoved = (from tenantDeployment in tenantDeploymentsAfterRemoved
                                     group tenantDeployment by tenantDeployment.AppHostInstanceId
                                     into appHostGroup
                                     select
                                         new ApplicationHost
                                             {
                                                 ApphostHttpEndpoint = RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.First
                                                     (i => i.Id.Equals(appHostGroup.Key)).InstanceEndpoints[Settings.AppHostHttpEndpointName].IPEndpoint,
                                                 Deployments = appHostGroup,
                                                 InstanceId = appHostGroup.Key,
                                                 Status = ApplicationHostStatus.Running
                                             }).ToList();

            this.arrConfigManager.UpdateApplicationHostsConfiguration(hostsAfterRemoved);
            this.arrConfigManager.DeleteRoutingRules(removedDeployments);

            foreach (var tenantDeployment in removedDeployments)
            {
                var tableContext = TenantDeploymentsStore.Instance;
                try
                {
                    string appId = tenantDeployment.AppHostInstanceId;
                    string app = tenantDeployment.Application;
                    string host = tenantDeployment.HostName;

                    var deployment = tableContext.TenantDeployment.Where(
                        td => td.AppHostInstanceId.Equals(appId)
                              && td.Application.Equals(app)
                              && td.HostName.Equals(host)).ToList();

                    if (deployment.Count == 1)
                    {
                        tableContext.DeleteObject(deployment[0]);
                        tableContext.SaveChanges();
                    }
                }
                catch(Exception)
                {
                    
                }
            }
        }

        #region Sample XML Rewrite URL and ARR Farm/Server

        private static IEnumerable<ApplicationHost> GetAppHostsWithApplications()
        {
            var deployments = TenantDeploymentsStore.GetCurrentDeployments().ToList();
            var appHosts = (from deployment in deployments
                            group deployment by deployment.AppHostInstanceId
                                into deploymentGroup
                                let roleInstance = RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.FirstOrDefault(
               id => id.Id.Equals(deploymentGroup.Key))
                                where roleInstance != null
                                select new ApplicationHost
                                    {
                                        InstanceId = deploymentGroup.Key,
                                        ApphostHttpEndpoint =
                                            roleInstance.InstanceEndpoints[Settings.AppHostRoleName].IPEndpoint,
                                        Deployments = deploymentGroup,
                                        Status = ApplicationHostStatus.Running
                                    }).ToList();

            var instanceIds = from instance in RoleEnvironment.Roles[Settings.AppHostRoleName].Instances
                              select instance.Id;

            var appHostInstanceIds = appHosts.Select(host => host.InstanceId);

            var emptyHosts = instanceIds.Except(appHostInstanceIds);

            return appHosts.Concat((from emptyHost in emptyHosts
                                    select new ApplicationHost
                                        {
                                            InstanceId = emptyHost,
                                            ApphostHttpEndpoint =
                                                RoleEnvironment
                                                .Roles[Settings.AppHostRoleName]
                                                .Instances.First(id => id.Id.Equals(emptyHost))
                                                .InstanceEndpoints[Settings.AppHostRoleName]
                                                .IPEndpoint,
                                            Deployments = new List<TenantDeployment>(),
                                            Status = ApplicationHostStatus.Running
                                        }).ToList());
        }

        private static IEnumerable<ApplicationHost> CurrentAppHostsWithApplications()
        {
            var tenantDeployments = TenantDeploymentsStore.Instance.TenantDeployment.ToList();

            var appHostsWithDeployments = (from tenantDeployment in tenantDeployments
                                           where tenantDeployment.Status.Equals(ApplicationHostStatus.Running)
                                           group tenantDeployment by tenantDeployment.AppHostInstanceId
                                               into appHostGroup
                                               select
                                                   new ApplicationHost
                                                       {
                                                           ApphostHttpEndpoint =
                                                               RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.First(
                                                                   ins => ins.Id.Equals(appHostGroup.Key)).InstanceEndpoints[
                                                                       Settings.AppHostHttpEndpointName].IPEndpoint,
                                                           Status = ApplicationHostStatus.Running,
                                                           InstanceId = appHostGroup.Key,
                                                           Deployments = appHostGroup
                                                       }).ToList();
            return appHostsWithDeployments;
        }

        private void OnStatusChange(Guid requestId, string message)
        {
            var handler = this.StatusChange;
            if (handler != null)
            {
                handler(this, new RequestRoutingManagerEventArgs { RequestId = requestId, StatusMessage = message });
            }
        }

        private void UpdateTenantDeployments(IEnumerable<ApplicationDeploymentRequest> tenantChanges)
        {
            tenantChanges = tenantChanges.ToList();

            BowlusLogger.LogInfo(string.Format("Processing {0} tenantChange messages.", tenantChanges.Count()));
            foreach (var applicationDeploymentRequest in tenantChanges)
            {
                applicationDeploymentRequest.MakeDeploymentRequest();
            }
        }

        /*
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
        <system.webserver>
            <rewrite>
                <rules>
                    <clear />
                    <rule name="trent.bowlus.net" patternsyntax="ecmascript" stopprocessing="true">
                        <match url=".*" />
                        <conditions logicalgrouping="matchall" trackallcaptures="false">
                            <add input="{http_host}" pattern="trent.bowlus.net" />
                        </conditions>
                        <action type="rewrite" url="http://trenttest/{r:0}" />
                    </rule>
                    <rule name="webrolewebappfarm" stopprocessing="true">
                        <match url=".*" />
                        <conditions logicalgrouping="matchall" trackallcaptures="false" />
                        <action type="rewrite" url="http://fullscale180/{r:0}" />
                    </rule>
                </rules>
            </rewrite>
        </system.webserver>
    </configuration>

    <webFarms>
        <webFarm name="WebRoleWebAppFarm" enabled="true">
            <server address="10.115.80.73" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
            <server address="10.115.72.84" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
            <server address="10.115.88.49" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
            <applicationRequestRouting>
                <affinity useCookie="true" />
                <loadBalancing algorithm="WeightedRoundRobin" />
            </applicationRequestRouting>
        </webFarm>
        <webFarm name="TrentTest" enabled="true">
            <server address="10.115.88.49" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
        </webFarm>
        <applicationRequestRouting>
            <hostAffinityProviderList>
                <add name="Microsoft.Web.Arr.HostNameRoundRobin" />
                <add name="Microsoft.Web.Arr.HostNameMemory" />
            </hostAffinityProviderList>
        </applicationRequestRouting>
    </webFarms>
     */

        #endregion

        private void ProcessReroutes()
        {
            var needToReroute =
                TenantDeploymentsStore.Instance.TenantDeployment.Where(
                    td => td.DeploymentAction.Equals(DeploymentActionType.Reroute)).ToList();

            // Don't waste cycles, bail out if necessary
            if (needToReroute.Count == 0) return;

            var appHoststToRerouteTo = (from tenantDeployment in needToReroute
                                        group tenantDeployment by
                                            RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.First(
                                                i => i.Id.Equals(tenantDeployment.AppHostInstanceId)).InstanceEndpoints[Settings.AppHostHttpEndpointName]
                                            into appHostGroup
                                            where !this.applicationHosts.Values.Any(ah => ah.InstanceId.Equals(appHostGroup.Key.RoleInstance.Id))
                                            select
                                                new ApplicationHost
                                                    {
                                                        ApphostHttpEndpoint = appHostGroup.Key.IPEndpoint,
                                                        Deployments = appHostGroup,
                                                        InstanceId = appHostGroup.Key.RoleInstance.Id,
                                                        Status = ApplicationHostStatus.Running
                                                    }).ToList();

            var newAppHostList = appHoststToRerouteTo.Concat(this.applicationHosts.Values).ToList();

            this.arrConfigManager.UpdateApplicationHostsConfiguration(newAppHostList);

            this.arrConfigManager.UpdateRoutingRules(needToReroute);
            foreach (var tenantDeployment in needToReroute)
            {
                tenantDeployment.Status = TenantDeploymentStatus.Running;
                tenantDeployment.DeploymentAction = DeploymentActionType.None;
                TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployment);
            }
        }

		private int ProcessApplicationRemovals()
		{
			var tenantDeployments = TenantDeploymentsStore.Instance.TenantDeployment.ToList();

			var appsToBeDeleted = (from tenantDeployment in tenantDeployments
			                       where
			                       	tenantDeployment.DeploymentAction.Equals(
			                       		DeploymentActionType.DeleteApplicationFromHosting)
			                       select tenantDeployment).ToList();

			List<ApplicationHost> currentAppHosts;
			foreach (var appToDelete in appsToBeDeleted)
			{
				currentAppHosts = this.CurrentAppHosts(tenantDeployments).ToList();

				// First, remove
				var requests = this.applicationDeploymentStrategy.RemoveApplication(appToDelete, currentAppHosts);
				foreach (var request in requests)
				{
					request.MakeDeploymentRequest();
				}
			}

			if (appsToBeDeleted.Any())
			{
				// Then re-balance if necessary
				currentAppHosts = this.CurrentAppHosts(tenantDeployments).ToList();
				var moveRequests = this.applicationDeploymentStrategy.MoveApplications(currentAppHosts);
				foreach (var request in moveRequests)
				{
					request.MakeDeploymentRequest();
				}
			}

			return appsToBeDeleted.Count;
		}

    	private int ProcessApplicationAdditions()
        {
            var tenantDeployments = TenantDeploymentsStore.Instance.TenantDeployment.ToList();

            var applicationsAdded = (from tenantDeployment in tenantDeployments
                                     where
                                         tenantDeployment.DeploymentAction.Equals(
                                             DeploymentActionType.AddApplicationForHosting)
                                     select tenantDeployment).ToList();

            foreach (var newApplication in applicationsAdded)
            {
                var currentAppHosts = this.CurrentAppHosts(tenantDeployments);
                var requests = this.applicationDeploymentStrategy.AddApplication(newApplication, currentAppHosts);
                foreach (var request in requests)
                {
                    request.MakeDeploymentRequest();
                }
            }

            return applicationsAdded.Count;
        }

        private IEnumerable<ApplicationHost> CurrentAppHosts(IEnumerable<TenantDeployment> tenantDeployments)
        {
			var currentAppHosts =
				(from appHost in this.applicationHosts.Values
				 where appHost.Status.Equals(ApplicationHostStatus.Running)
				 select appHost).ToList();

			var appHostsWithDeployments = (from tenantDeployment in tenantDeployments
										   where tenantDeployment.Status.Equals(TenantDeploymentStatus.Running)
										   group tenantDeployment by tenantDeployment.AppHostInstanceId
											   into appHostGroup
											   select
												   new ApplicationHost
													   {
														   ApphostHttpEndpoint =
															   RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.
															   First
															   (ins => ins.Id.Equals(appHostGroup.Key)).InstanceEndpoints[
																   Settings.AppHostHttpEndpointName].IPEndpoint,
														   Status = ApplicationHostStatus.Running,
														   InstanceId = appHostGroup.Key,
														   Deployments = appHostGroup
													   }).ToList();

        	foreach (var host in currentAppHosts)
        	{
        		if(!appHostsWithDeployments.Any(ah => ah.InstanceId.Equals(host.InstanceId)))
        		{
        			appHostsWithDeployments.Add(host);
        		}
        	}

            return appHostsWithDeployments;
        }

        private int ProcessAppHostAdditions()
        {
            var appHostsAdded =
                this.applicationHosts.Values.Where(ah => ah.Status.Equals(ApplicationHostStatus.Added)).ToList();

            foreach (var appHost in appHostsAdded)
            {
                var currentAppHostsWithApplications = CurrentAppHostsWithApplications();
                var requests = this.applicationDeploymentStrategy.AddHost(appHost, currentAppHostsWithApplications);
                foreach (var request in requests)
                {
                    request.MakeDeploymentRequest();
                }
            }

            return appHostsAdded.Count;
        }

        private int ProcessAppHostRemovals()
        {
            // Start with the apphosts that need to be deleted
            var appHostsToBeDeleted =
                this.applicationHosts.Values.Where(
                    ah =>
                    ah.Status.Equals(ApplicationHostStatus.Deleting) || ah.Status.Equals(ApplicationHostStatus.Deleted))
                    .ToList();

            if (!appHostsToBeDeleted.Any()) return appHostsToBeDeleted.Count;

            // Only master will decide what to do with this host being removed.
            // Reroutes will evantually be requested by the appHost and will be received in 
            // the same poll loop to be processed by non-master FrontEnds
            if (this.IsMaster)
            {
                foreach (var applicationHost in appHostsToBeDeleted)
                {
                    var currentAppHostsWithApplications = CurrentAppHostsWithApplications();
                    var requests = this.applicationDeploymentStrategy.RemoveHost(applicationHost, currentAppHostsWithApplications);
                    foreach (var request in requests)
                    {
                        request.MakeDeploymentRequest();
                    }
                }
            }

            foreach (var host in appHostsToBeDeleted)
            {
                ApplicationHost removedHost;
                this.applicationHosts.TryRemove(host.ApphostHttpEndpoint.Address.ToString(), out removedHost);
            }

            this.arrConfigManager.UpdateApplicationHostsConfiguration(appHostsToBeDeleted);

            return appHostsToBeDeleted.Count;
        }
    }
}
